¿Qué es una función?

Sabemos lo que es una función en programación. Muy esquematicamente, una función es una forma de reutilizar nuestro código sin tener que repetir las mismas instrucciones. Una función nos permite guardar una serie de instrucciones para usarlas sobre nuestros datos cada vez que lo necesitemos.

def mensaje():
  print("Hola soy un mensaje.")
  print("La vida de las mensajes es corta.")
  print("Aparecemos en pantalla y morimos.")

mensaje()
Hola soy un mensaje.
La vida de las mensajes es corta.
Aparecemos en pantalla y morimos.

En este ejemplo, nuestra función mensaje() solo hace una serie de instrucciones. Esta función, no tiene argumentos y no devuelve ningun tipo de valores o resultado.

Si bien esto es una función en el sentido de lenguajes de programación, no es una función matemática

Esta diferencia entre una función matemática y una serie de procedimientos era tenida en lenguajes de programación como Fortran (creado en la decada del 50). La razón para introducir esta diferencia entre función matemática y función en lenguajes de computación es para poder hablar de cálculo diferencial.

Una función matemática, a los términos prácticos, es una caja negra. Como caja negra, nuestra función recibe una entrada definida, devuelve una una salida definida. Tanto la entrada como la salida deben ser valores. En caso contrario, no es una función en el sentido matemático.

import random

def NoFuncion1():
  print("No soy una función.")
  print("No tengo entradas ni salidas")

def NoFuncion2(a):
  print("No soy una función.")
  print("Tengo entradas: ", a)
  print("Pero, no tengo salidas")

def NoFuncion3():
  result = 1
  print("No soy una función.")
  print("Tengo salidas: ", result)
  print("Pero, no tengo entradas")
  return result

def NoFuncion4(a):
  result = random.randrange(10)
  print("No soy una función.")
  print("Tengo entradas: ", a)
  print("Tengo salidas: ", result)
  print("Pero mi salida no está definida.")
  print("Mi salida es aleatoria")
  return result

def ProperFuncion4(a):
  result = 1
  print("No soy una función.")
  print("Tengo entradas: ", a)
  print("Tengo salidas: ", result)
  return result

Otra diferencia entre funciones dentro de lenguajes de programáción y funciones matemáticas es que las funciones matemáticas no prescriben la forma de calculo

def duplicate(x : int):
  return 2 * x

def duplicar(x : int):
  result = x
  for i in range(x):
    result += 1
  return result

def daburu(x : int):
  x += x
  return x

daburu(2), duplicate(2), duplicar(2)
(4, 4, 4)

En este caso hemos definido 3 funciones. Todas ellas calculan el doble de un número. pero lo hacen de 3 maneras distintas:

  • duplicate obtiene el doble por medio de multiplicaciones

  • duplicar obtiene el doble con incrementos constantes. Estos incrementos constantes siempre valen 1. Este aumento se realiza varias veces, hasta llegar al resultado

  • daburu obtiene el doble por medio sumas

La definición de función matemática no dice como debemos calculara la función. Solo nos relaciona entradas con salidas. En este sentido, las funciones matemáticas son una abtracción mucho mayor y por esto decimos que se comportan como verdaderas cajas negras.

Cuando empezamos a trabajar con Python, dijimos que Python es un lenguaje de tipado dinámico. Es decir, una variable puede representar un número en un momento, una cadena de caracteres en otro y una lista en otros.

Si uno quisiera comparar funciones en Python con las funciones matemáticas, un debería decir que las funciones matemáticas se comportan como lenguajes de tipado estricto. Si la entrada a una función son números enteros, la función no puede recibir números reales.

# Ejemplo de tipado dinámico

a = "mensaje"
print(a)
a = 2.2
print(a)
a = 10
print(a)
a = [1,0]
print(a)
mensaje
2.2
10
[1, 0]

En tipado estático, si la variable a era un string, no podemos asignar nada que no sea string. La función print, también tiene algo similar al tipado dinámico. Recibe la variable a y hace algo, una función matemática no podría hacer nada si a no fuera del tipo correcto.

Por ejemplo, en el lenguaje C, la función printf debe recibir siempre como primer argumento una cadena de caracteres. Caso contrario, el programa fallará. En C, printf es una función en el sentido matemático. La salida de printf es la cantidad de caracteres enviados a la pantalla.

Este ultimo punto sobre las diferencias entre funciones matemáticas y funciones en lenguajes de programción es muy importante. Para hablar de cálculo diferencial necesitamos que nuestra entrada sean números reales o cosas que se comportan como los números reales, por ejemplo vectores de componentes reales. Es por esto que en cálculo diferencial solo le interesan las funciones con entradas reales y salidas reales. Desde el punto de vista matemático, son las propiedades de los números reales, las que dan sentido al cálculo.

Calculo diferencial

El cálculo es una ráma de la mátemática que surge para poder estudiar el problema del cambio. Desde la antigua grecia, existían planteos de sentido común que señalaban las limitaciones de la matemática de la época. Las paradojas de Zenon son ejemplo de esas objeciones.

Estas objeciones tardaron varios siglos en ser respondidas. Sin embargo, casi todas ellas fueron resultas con el cálculo. El cálculo ha sido la principal herramienta científica desde que Liebniz y Newton lo desarrollaron en el siglo XVII. Sin embargo, algunos precedentes a la disciplicna están presentes en los trabajos de Arquimedes, Galileo e incluso en los desarrollos de matemáticos indios e chinos.

Steven Strogatz en su libro “Infinite Powers” afirma que en esencia el cálculo parte de 2 ideas. Cortar todo en pedacitos, para analizar y entender los pedacitos. Luego, juntar los pedacitos para tener una mejor compresión de la totalidad del problema.

El primer problema, el de “cortar todo en pedacitos y analizarlos” es conocido con el nombre de cálculo diferencial. El cálculo diferencial es una herramienta que nos permite encontrar mínimos, máximos y otras solucioens óptimas. En este sentido, el cálculo diferencial es una herramienta clave en Machine Learning. Tal es así, que todo modelo, tarde o temprano tratara de minimizar o maximizar alguna cantidad.

Warning: this notebook accompanies the 1st edition of the book. Please visit https://github.com/ageron/handson-ml2 for the 2nd edition code, with up-to-date notebooks using the latest library versions.

Pendiente de una línea recta

#@title
%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np

# To get smooth animations
import matplotlib.animation as animation
mpl.rc('animation', html='jshtml')

¿Cuál es la pendiente de la siguiente recta?

#@title
def get_AB_line(A_pos, B_pos, x_min=-1000, x_max=+1000):
    rise = B_pos[1] - A_pos[1]
    run = B_pos[0] - A_pos[0]
    slope = rise / run
    offset = A_pos[1] - slope * A_pos[0]
    return [x_min, x_max], [x_min * slope + offset, x_max * slope + offset]

def plot_AB_line(A_pos, B_pos, A_name="A", B_name="B"):
    for point, name in ((A_pos, A_name), (B_pos, B_name)):
        plt.plot(point[0], point[1], "bo")
        plt.text(point[0] - 0.35, point[1], name, fontsize=14)
    xs, ys = get_AB_line(A_pos, B_pos)
    plt.plot(xs, ys)

def plot_rise_over_run(A_pos, B_pos):
    plt.plot([A_pos[0], B_pos[0]], [A_pos[1], A_pos[1]], "k--")
    plt.text((A_pos[0] + B_pos[0]) / 2, A_pos[1] - 0.4, "cambio horizontal", fontsize=14)
    plt.plot([B_pos[0], B_pos[0]], [A_pos[1], B_pos[1]], "k--")
    plt.text(B_pos[0] + 0.2, (A_pos[1] + B_pos[1]) / 2, "cambio vertical", fontsize=14)

def show(axis="equal", ax=None, title=None, xlabel="$x$", ylabel="$y$"):
    ax = ax or plt.gca()
    ax.axis(axis)
    ax.grid()
    ax.set_title(title, fontsize=14)
    ax.set_xlabel(xlabel, fontsize=14)
    ax.set_ylabel(ylabel, fontsize=14, rotation=0)
    ax.axhline(y=0, color='k')
    ax.axvline(x=0, color='k')

A_pos = np.array([1, 1])
B_pos = np.array([7, 4])
plot_AB_line(A_pos, B_pos)
plot_rise_over_run(A_pos, B_pos)
show([0, 8.4, 0, 5.5], title="pendiente = cambio verical / cambio horizontal")
../../_images/Funciones2_18_0.png

La noción de pendiente, en algun sentido es una noción intuitiva para una línea recta. Dados dos puntos cualesquiera de la recta, \(\mathrm{A}\) y \(\mathrm{B}\), solo tenemos que ver el cambio vertical y el cambio horizontal.:

\(\mathrm{pendiente} = \dfrac{\mathrm{cambio~vertical}}{\mathrm{cambio~horizontal}} = \dfrac{\Delta y}{\Delta x} = \dfrac{y_\mathrm{B} - y_\mathrm{A}}{x_\mathrm{B} - x_\mathrm{A}}\)

En el ejemplo anterior, tenemos \({\Delta y = 3}\), \({\Delta x=6}\), por lo tanto la pendiente es \(0.5\)

Pendiente de una curva arbitraria

Si bien la noción de pendiente es casi evidente para una recta, los matemáticos muchas veces no les basta con lo evidente. Quieren usar definiciones que les permitan generalizar ideas intuitivas a contextos menos obvios. Por ejemplo, dada la función \(y = f(x) = x^2\), ¿cómo encontramos una pendiente?

#@title
xs = np.linspace(-2.1, 2.1, 500)
ys = xs**2
plt.plot(xs, ys)

plt.plot([0, 0], [0, 3], "k--")
plt.arrow(-1.4, 2.5, 0.5, -1.3, head_width=0.1)
plt.arrow(0.85, 1.05, 0.5, 1.3, head_width=0.1)
show([-2.1, 2.1, 0, 2.8], title="Pendiente de $y = x^2$")
../../_images/Funciones2_22_0.png

En nuestro primer ejemplo, la pendiente de la recta era positiva. Esto indicaba la función era creciente. A la izquierda de \(x = 0\), la función \(f(x) = x^2\) es decreciente, mientras que a la derecha es creciente. A partir de esto podemos inferir lo siguiente: la definición de “pendiente de una curva” debe cumplir dos condiciones:

  1. la pendiente de una curva es positiva cuando la función es creciente.

  2. la pendiente de una curva es negativa cuando la función es decreciente.

De hecho, podemos intuir que la definición de “pendiente de una curva” esta relacionada con la pendiente de una recta tangente a un punto de la curva. Esto no es azaroso, la definición de la pendiente de una curva en un punto, es la pendiente de la recta tangente a ese punto.

Además, está definición nos indica que en el vértice de \(f(x) = x^2\) la pendiente debe ser \(0\). Este punto fue uno de los grandes descubrimientos de Fermat. Los mínimos o máximos locales tienen pendiente nula. Esta observación es la que nos permite decir que un modelo de Machine Learning tiene parámetros optimos para los datos que tenemos.

Sin embargo estamos en un impasse. Sabemos, desde los griegos, que dos puntos definen una recta. Pero aquí solo tenemos un punto que pasa por la recta. Dicho de otro modo. Podemos generar todas las rectas pasan por 2 puntos de la curva \(f(x)=x^2\). Pero, hasta ahora, no sabemos como encontrar una recta (tangente) que pase solo por un punto.

Este problema es parecido al que encontró Fermant en el siglo XVII. El problema de Fermat era: ¿Como encuento el punto donde la tangente tiene pendiente nula? Nuestro problema es ¿como encontramos la pendiente de la recta tangente a un punto? Ambos problemas necesitan que aprendamos a calcular la pendiente a partir de una función.

La solución de Fermat era mucho más intuitiva que la propuesta que discutiremos a continuación. Pero en el fondo su propuesta es equivalente a la nuestra. La ventaja de lo que mostraremos a continuación es que permite generalizar de manera directa el problema. Por otro lado la propuesta de Fermat estaba pensada solo para el problema de la optimización. Debieron pasar varias décadas para que los matemáticos europeos encontraran la forma de generalizar las ideas de Fermat y casi dos siglos para encontrar una definición formal.

Nuestra propuesta es intuitiva y austera. Dado que no podemos calcular la pendiente de la tangente en \(\mathrm{A}\), \(x_A = -1\), calculemos una secante que para por \(x_A = -1\) y otro punto \(\mathrm{B}\). Luego tratemos de acercar el punto \(\mathrm{B}\) hasta el punto \(\mathrm{A}\) sin que nunca coincidan.

#@title
def animate_AB_line(f, fp, f_str, x_A, axis=None):
    y_A = f(x_A)
    eps = 1e-4
    x_B_range = 1.5
    x_B = x_A + eps

    n_frames = 200
    text_offset_A = -0.2
    text_offset_B = +0.1
    x_min, x_max = -1000, 1000

    fig, ax = plt.subplots()

    # plot f(x)
    xs = np.linspace(-2.1, 2.1, 500)
    ys = f(xs)
    ax.plot(xs, ys)

    # plot the tangent to the curve at point A
    if fp:
        slope = fp(x_A)
        offset = y_A - slope * x_A
        ax.plot([x_min, x_max], [slope*x_min + offset, slope*x_max + offset],
                "y--")

    # plot the line AB and the labels A and B so they can be animated
    y_A = f(x_A)
    y_B = f(x_B)
    xs, ys = get_AB_line([x_A, y_A], [x_B, y_B])
    line_inf, = ax.plot(xs, ys, "-")
    line_AB, = ax.plot([x_A, x_B], [y_A, y_B], "bo-")
    ax.text(x_A + text_offset_A, y_A, "A", fontsize=14)
    B_text = ax.text(x_B + text_offset_B, y_B, "B", fontsize=14)

    # plot the grid and axis labels
    title = r"Pendiente de $y = {}$ en $x_\mathrm{{A}} = {}$".format(f_str, x_A)
    show(axis or [-2.1, 2.1, 0, 2.8], title=title)

    def update_graph(i):
        x_B = x_A + x_B_range * np.cos(i * 2 * np.pi / n_frames) ** 3
        if np.abs(x_B - x_A) < eps:
            x_B = x_A + eps # to avoid division by 0
        y_B = f(x_B)
        xs, ys = get_AB_line([x_A, y_A], [x_B, y_B])
        line_inf.set_data(xs, ys)
        line_AB.set_data([x_A, x_B], [y_A, y_B])
        B_text.set_position([x_B + text_offset_B, y_B])
        return line_inf, line_AB

    anim = animation.FuncAnimation(fig, update_graph,
                                  init_func=lambda: update_graph(0),
                                  frames=n_frames,
                                  interval=20,
                                  blit=True)
    plt.close()
    return anim

animate_AB_line(lambda x: x**2, lambda x: 2*x, "x^2", -1)

Vemos que nos acercamos a la recta tangente al punto \(\mathrm{A}\). En la proximas secciones, hablaremos de como se puede encontrar esta pendiete de manera analítica. Pero por ahora, lo que diremos que encontrar esta pendiente se conoce como encontrar la derivada a un punto de una curva. Diremos que la función que nos da la pendiente para cada valor de \(x\) es la función derivada o la derivada de \(f(x)\). En concreto, para este problema la derivada en \(x=x_\mathrm{A}\) vale \(2x_\mathrm{A}\)

Diferenciabilidad

Tratemos de usar lo que aprendimos a funciones más complejas. Por ejeplo \(f(x)=|x|\), valor absoluto de \(x\):

#@title
animate_AB_line(lambda x: np.abs(x), None, "|x|", 0)

A la derecha de \(x=0\) es claro que la pendiente es \(1\), y a la izquierda es \(-1\). Sin embargo, el cambio es discreto. No hay una transición lenta de \(1\) a \(-1\). Por el contrario, es abrupta. Además, ese cambio hace que no podemos definir una tángente en \(x=0\) Hay otras funciones que tienen estas propiedades, ya sea en \(x=0\) o en otros puntos. En todos los casos, cuando no se puede definir una recta tangente a un punto, se dice que la función es no es diferenciable en ese punto. \(|f(x)|\) es no diferenciable en \(x=0\). Hay casos extremos de funciones no diferenciables en ningun punto. Pero por ahora solo interesa saber que no toda función es diferenciable en todos sus puntos.

esconder

This implies several constraints:

  • First, the function must of course be defined at \(x_\mathrm{A}\). As a counterexample, the function \(f(x)=\dfrac{1}{x}\) is undefined at \(x_\mathrm{A}=0\), so it is not differentiable at that point.

  • The function must also be continuous at \(x_\mathrm{A}\), meaning that as \(x_\mathrm{B}\) gets infinitely close to \(x_\mathrm{A}\), \(f(x_\mathrm{B})\) must also get infinitely close to \(f(x_\mathrm{A})\). As a counterexample, \(f(x)=\begin{cases}-1 \text{ if }x < 0\\+1 \text{ if }x \geq 0\end{cases}\) is not continuous at \(x_\mathrm{A}=0\), even though it is defined at that point: indeed, when you approach it from the negative side, it does not approach infinitely close to \(f(0)=+1\). Therefore, it is not continuous at that point, and thus not differentiable either.

  • The function must not have a breaking point at \(x_\mathrm{A}\), meaning that the slope that the \((\mathrm{AB})\) line approaches as \(\mathrm{B}\) approaches \(\mathrm{A}\) must be the same whether \(\mathrm{B}\) approaches from the left side or from the right side. We already saw a counterexample with \(f(x)=|x|\), which is both defined and continuous at \(x_\mathrm{A}=0\), but which has a breaking point at \(x_\mathrm{A}=0\): the slope of the curve \(y=|x|\) is -1 on the left, and +1 on the right.

  • The curve \(y=f(x)\) must not be vertical at point \(\mathrm{A}\). One counterexample is \(f(x)=\sqrt[3]{x}\), the cubic root of \(x\): the curve is vertical at the origin, so the function is not differentiable at \(x_\mathrm{A}=0\), as you can see in the following animation:

#@title
animate_AB_line(lambda x: np.cbrt(x), None, r"\sqrt[3]{x}", 0,
                axis=[-2.1, 2.1, -1.4, 1.4])

Now let’s see how to actually differentiate a function (i.e., find its derivative).

Derivada de una función

Toda nuestra discusión sobre pendientes, secantes y recta implicitamente nos pide que calculemos una pendiente de la siguiente manera

\(\dfrac{y_\mathrm{B} - y_\mathrm{A}}{x_\mathrm{B} - x_\mathrm{A}}\)

Pero como \(y = f(x)\), tenemos:

\(\dfrac{f(x_\mathrm{B}) - f(x_\mathrm{A})}{x_\mathrm{B} - x_\mathrm{A}}\)

Ahora lo que queremos es que \(x_\mathrm{B}\) se acerque a \(x_\mathrm{A}\), pero sin alcanzarlo nunca. Si lo alcanzara tendríamos que dividir por \(0\) y sabemos que no podemos hacer eso. Este problema de “acercarse” sin tocar es algo que los mátemáticos hacen todo el tiempo y recibe el nombre de límite.

\(\underset{x_\mathrm{B} \to x_\mathrm{A}}\lim\dfrac{f(x_\mathrm{B}) - f(x_\mathrm{A})}{x_\mathrm{B} - x_\mathrm{A}}\)

En efecto esa es la definición de derivada de una función \(f\)

\(f'(x_\mathrm{A}) = \underset{x_\mathrm{B} \to x_\mathrm{A}}\lim\dfrac{f(x_\mathrm{B}) - f(x_\mathrm{A})}{x_\mathrm{B} - x_\mathrm{A}}\)

Ejemplo: derivada de \(f(x) = x^2\)

Hagamos un ejemplo de como uno aplica esta definición para \(f(x) = x^2\)

\(\begin{split} f'(x_\mathrm{A}) \, && = \underset{x_\mathrm{B} \to x_\mathrm{A}}\lim\dfrac{f(x_\mathrm{B}) - f(x_\mathrm{A})}{x_\mathrm{B} - x_\mathrm{A}} \\ && = \underset{x_\mathrm{B} \to x_\mathrm{A}}\lim\dfrac{{x_\mathrm{B}}^2 - {x_\mathrm{A}}^2}{x_\mathrm{B} - x_\mathrm{A}} \quad && \text{dado que } f(x) = x^2 \\ && = \underset{x_\mathrm{B} \to x_\mathrm{A}}\lim\dfrac{(x_\mathrm{B} - x_\mathrm{A})(x_\mathrm{B} + x_\mathrm{A})}{x_\mathrm{B} - x_\mathrm{A}}\quad && \text{dif. de cuadrados } {x_\mathrm{A}}^2 - {x_\mathrm{B}}^2 = (x_\mathrm{A}-x_\mathrm{B})(x_\mathrm{A}+x_\mathrm{B}) \\ && = \underset{x_\mathrm{B} \to x_\mathrm{A}}\lim(x_\mathrm{B} + x_\mathrm{A})\quad && \text{simplificamos } (x_\mathrm{B} - x_\mathrm{A}) \text{, sabemos que son distintos a 0} \\ && = x_\mathrm{A} + x_\mathrm{A}\quad && \text{dado que nada nos impide cumplir el limite ahora} \\ && = 2 x_\mathrm{A} \end{split}\)

Otra definición cómun es pedir que el límite use un parametro que se acerque a \(0\)

\(f'(x_\mathrm{A}) = \underset{\epsilon \to 0}\lim\dfrac{f(x_\mathrm{A} + \epsilon) - f(x_\mathrm{A})}{\epsilon}\)

O incluso pedir que sea valido para cualquier valor de la variable

\(f'(x) = \underset{\epsilon \to 0}\lim\dfrac{f(x + \epsilon) - f(x)}{\epsilon}\)